﻿namespace Webinator.Toolboxes
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Text.RegularExpressions;
    using System.Xml;
    using System.Xml.XPath;
    using Enums;
    using ExtensionMethods;
    using HtmlAgilityPack;
    using MultipleAttribute;
    using Webinator.DomTraversal;
    using Webinator.WebClient;

    /// <summary>
    /// Contains useful XPath functionality.
    /// </summary>
    public static class XPathToolbox
    {
        /// <summary>
        /// The css2xpath transformer.
        /// </summary>
        private static readonly Css2Xpath _cx;

        /// <summary>
        /// Node dictionary.
        /// </summary>
        private static readonly Dictionary<NodeIs, string> _nodes = new Dictionary<NodeIs, string>
        {
            { NodeIs.Anchor, "a" },
            { NodeIs.Body, "body" },
            { NodeIs.Button, "button" },
            { NodeIs.Caption, "caption" },
            { NodeIs.Column, "col" },
            { NodeIs.ColumnGroup, "colgroup" },
            { NodeIs.Div, "div" },
            { NodeIs.Fieldset, "fieldset" },
            { NodeIs.Form, "form" },
            { NodeIs.Head, "head" },
            { NodeIs.Html, "html" },
            { NodeIs.Image, "img" },
            { NodeIs.Input, "input" },
            { NodeIs.Label, "label" },
            { NodeIs.ListItem, "li" },
            { NodeIs.OrderedList, "ol" },
            { NodeIs.OptionGroup, "optgroup" },
            { NodeIs.Option, "option" },
            { NodeIs.Paragraph, "p" },
            { NodeIs.Select, "select" },
            { NodeIs.Span, "span" },
            { NodeIs.Table, "table" },
            { NodeIs.TableBody, "tbody" },
            { NodeIs.TableCell, "td" },
            { NodeIs.Textarea, "textarea" },
            { NodeIs.TableFooter, "tfoot" },
            { NodeIs.TableHeaderCell, "th" },
            { NodeIs.TableHeader, "thead" },
            { NodeIs.Title, "title" },
            { NodeIs.TableRow, "tr" },
            { NodeIs.UnorderedList, "ul" }
        };

        /// <summary>
        /// Initializes static members of the <see cref="XPathToolbox"/> class.
        /// </summary>
        static XPathToolbox()
        {
            _cx = new Css2Xpath();
        }

        /// <summary>
        /// Returns a DOM tree parser for the specified html.
        /// </summary>
        /// <param name="html">The html.</param>
        /// <returns>The DOM tree parser for the specified html.</returns>
        public static DomNode GetDom(string html)
        {
            return new DomNode(html);
        }

        /// <summary>
        /// Returns a DOM tree parser for the specified html.
        /// </summary>
        /// <param name="html">The html.</param>
        /// <returns>The DOM tree parser for the specified html.</returns>
        public static DomNode GetDom(HtmlDocument html)
        {
            return new DomNode(html.DocumentNode.OuterHtml);
        }

        /// <summary>
        /// Gets the innerhtml from all nodes found at the XPath and constructs a multi-line text string.
        /// </summary>
        /// <param name="source">The html source.</param>
        /// <param name="xpath">The xpath.</param>
        /// <returns>
        /// A string containing the multiline text.
        /// </returns>
        public static string GetMultilineText(string source, string xpath)
        {
            string multilineText = string.Empty;
            var nodes = GetNodes(source, xpath);
            foreach (var node in nodes)
            {
                if (string.IsNullOrEmpty(multilineText))
                {
                    multilineText = node.InnerHtml;
                }
                else
                {
                    multilineText += string.Format("\r\n{0}", ReplaceKnownCharacterCodes(node.InnerHtml));
                }
            }

            return multilineText;
        }

        /// <summary>
        /// Evaluates the XPath expression on the html passed.
        /// </summary>
        /// <param name="source">The html.</param>
        /// <param name="xpath">The XPath.</param>
        /// <returns>The inner html found on the first node.</returns>
        public static string GetText(string source, string xpath)
        {
            return GetTexts(source, xpath).FirstOrDefault();
        }

        /// <summary>
        /// Evaluates the XPath expression on the html passed.
        /// </summary>
        /// <param name="source">The html.</param>
        /// <param name="xpath">The XPath.</param>
        /// <returns>The inner html found on all nodes, returned as a string array.</returns>
        public static IEnumerable<string> GetTexts(string source, string xpath)
        {
            var nodes = GetNodes(source, xpath);
            var innerHtml = new List<string>();

            if (nodes != null && nodes.Count > 0)
            {
                foreach (var node in nodes)
                {
                    if (node != null)
                    {
                        innerHtml.Add(ReplaceKnownCharacterCodes(node.InnerHtml));
                    }
                }
            }

            return innerHtml;
        }

        /// <summary>
        /// Evaluates the XPath expression on the html passed.
        /// </summary>
        /// <param name="html">The html.</param>
        /// <param name="xpath">The XPath.</param>
        /// <param name="attribute">The attribute.</param>
        /// <returns>The value of the attribute found on all nodes, returned as a string array.</returns>
        public static IEnumerable<string> GetAttributeValues(string html, string xpath, string attribute)
        {
            var nodes = GetNodes(html, xpath);
            var attributes = new List<string>();

            if (nodes != null && nodes.Count > 0)
            {
                foreach (var node in nodes)
                {
                    if (node != null)
                    {
                        attributes.Add(ReplaceKnownCharacterCodes(node.Attributes[attribute].Value));
                    }
                }
            }

            return attributes;
        }

        /// <summary>
        /// Evaluates the XPath expression on the html passed.
        /// </summary>
        /// <param name="html">The html.</param>
        /// <param name="xpath">The XPath.</param>
        /// <param name="attribute">The attribute.</param>
        /// <returns>The value of the attribute found on all nodes, returned as a string array.</returns>
        public static IEnumerable<string> GetAttributeValues(HtmlDocument html, string xpath, string attribute)
        {
            var nodes = GetNodes(html, xpath);
            var attributes = new List<string>();

            if (nodes != null && nodes.Count > 0)
            {
                foreach (var node in nodes)
                {
                    if (node != null)
                    {
                        attributes.Add(ReplaceKnownCharacterCodes(node.Attributes[attribute].Value));
                    }
                }
            }

            return attributes;
        }

        /// <summary>
        /// Evaluates the XPath expression on the html passed.
        /// </summary>
        /// <param name="html">The html.</param>
        /// <param name="xpath">The XPath.</param>
        /// <param name="attribute">The attribute.</param>
        /// <returns>The value of the attribute found on the node.</returns>
        public static string GetAttributeValue(string html, string xpath, string attribute)
        {
            return GetAttributeValues(html, xpath, attribute).First();
        }

        /// <summary>
        /// Evaluates the XPath expression on the html passed.
        /// </summary>
        /// <param name="html">The html.</param>
        /// <param name="xpath">The XPath.</param>
        /// <param name="attribute">The attribute.</param>
        /// <returns>The value of the attribute found on the node.</returns>
        public static string GetAttributeValue(HtmlDocument html, string xpath, string attribute)
        {
            var node = html.DocumentNode.SelectSingleNode(xpath);

            return GetAttributeValue(node, attribute);
        }

        /// <summary>
        /// Evaluates the XPath expression on the html passed.
        /// </summary>
        /// <param name="node">The node.</param>
        /// <param name="attribute">The attribute.</param>
        /// <returns>The value of the attribute found on the node.</returns>
        public static string GetAttributeValue(HtmlNode node, string attribute)
        {
            var value = node.Attributes[attribute].Value;

            return ReplaceKnownCharacterCodes(value);
        }

        /// <summary>
        /// Evaluates the XPath expression on the html passed.
        /// </summary>
        /// <param name="html">The html.</param>
        /// <param name="xpath">The XPath.</param>
        /// <param name="attribute">The attribute.</param>
        /// <param name="newValue">The new value to apply to the attribute.</param>
        /// <returns>The updated html.</returns>
        public static string SetAttributeValue(string html, string xpath, string attribute, string newValue)
        {
            var htmlDoc = GetHtmlDocument(html);
            htmlDoc = SetAttributeValue(htmlDoc, xpath, attribute, newValue);
            html = htmlDoc.DocumentNode.OuterHtml;

            return html;
        }
        
        /// <summary>
        /// Evaluates the XPath expression on the html passed.
        /// </summary>
        /// <param name="node">The node.</param>
        /// <param name="attribute">The attribute.</param>
        /// <param name="newValue">The new value to apply to the attribute.</param>
        /// <returns>The updated node.</returns>
        public static HtmlNode SetAttributeValue(HtmlNode node, string attribute, string newValue)
        {
            node.Attributes[attribute].Value = newValue;

            return node;
        }
        
        /// <summary>
        /// Evaluates the XPath expression on the html passed.
        /// </summary>
        /// <param name="html">The html.</param>
        /// <param name="xpath">The XPath.</param>
        /// <param name="attribute">The attribute.</param>
        /// <param name="newValue">The new value to apply to the attribute.</param>
        /// <returns>The updated html.</returns>
        public static HtmlDocument SetAttributeValue(HtmlDocument html, string xpath, string attribute, string newValue)
        {
            var node = html.DocumentNode.SelectSingleNode(xpath);
            SetAttributeValue(node, attribute, newValue);

            return html;
        }

        /// <summary>
        /// Evaluates the XPath expression on the html passed.
        /// </summary>
        /// <param name="nodes">The nodes.</param>
        /// <param name="attribute">The attribute.</param>
        /// <param name="newValue">The new value to apply to the attribute.</param>
        /// <returns>The updated node.</returns>
        public static IEnumerable<HtmlNode> SetAttributesValue(HtmlNode[] nodes, string attribute, string newValue)
        {
            foreach (var node in nodes)
            {
                node.Attributes[attribute].Value = newValue;
            }

            return nodes;
        }

        /// <summary>
        /// Evaluates the XPath expression on the html passed.
        /// </summary>
        /// <param name="html">The html.</param>
        /// <param name="xpaths">The XPath.</param>
        /// <param name="attribute">The attribute.</param>
        /// <param name="newValue">The new value to apply to the attribute.</param>
        /// <returns>The updated html.</returns>
        public static string SetAttributesValue(string html, IEnumerable<string> xpaths, string attribute, string newValue)
        {
            var htmlDoc = GetHtmlDocument(html);
            htmlDoc = SetAttributesValue(htmlDoc, xpaths, attribute, newValue);
            html = htmlDoc.DocumentNode.OuterHtml;

            return html;
        }

        /// <summary>
        /// Evaluates the XPath expression on the html passed.
        /// </summary>
        /// <param name="html">The html.</param>
        /// <param name="xpath">The XPath.</param>
        /// <param name="attribute">The attribute.</param>
        /// <param name="newValue">The new value to apply to the attribute.</param>
        /// <returns>The updated html.</returns>
        public static string SetAttributesValue(string html, string xpath, string attribute, string newValue)
        {
            return SetAttributesValue(html, new[] { xpath }, attribute, newValue);
        }

        /// <summary>
        /// Evaluates the XPath expression on the html passed.
        /// </summary>
        /// <param name="html">The html.</param>
        /// <param name="xpath">The XPath.</param>
        /// <param name="attribute">The attribute.</param>
        /// <param name="newValue">The new value to apply to the attribute.</param>
        /// <returns>The updated html.</returns>
        public static HtmlDocument SetAttributesValue(HtmlDocument html, string xpath, string attribute, string newValue)
        {
            return SetAttributesValue(html, new[] { xpath }, attribute, newValue);
        }

        /// <summary>
        /// Evaluates the XPath expression on the html passed.
        /// </summary>
        /// <param name="html">The html.</param>
        /// <param name="xpaths">The XPath expressions.</param>
        /// <param name="attribute">The attribute.</param>
        /// <param name="newValue">The new value to apply to the attribute.</param>
        /// <returns>The updated html.</returns>
        public static HtmlDocument SetAttributesValue(HtmlDocument html, IEnumerable<string> xpaths, string attribute, string newValue)
        {
            var nodes = GetNodes(html, xpaths);
            SetAttributesValue(nodes.ToArray(), attribute, newValue);

            return html;
        }

        /// <summary>
        /// Parses the html for the XPath.
        /// </summary>
        /// <param name="html">The html to parse.</param>
        /// <param name="xpath">The XPath.</param>
        /// <returns>
        /// The number of <c>HtmlNodes</c> found.
        /// </returns>
        public static int GetNodeCount(string html, string xpath)
        {
            var nodes = GetNodes(html, xpath);
            var count = nodes == null ? 0 : nodes.Count;
            return count;
        }

        /// <summary>
        /// Parses the html for the XPath expressions.
        /// </summary>
        /// <param name="html">The html to parse.</param>
        /// <param name="xpaths">The XPath expressions.</param>
        /// <returns>
        /// The <c>HtmlNodes</c> found.
        /// </returns>
        public static IEnumerable<HtmlNode> GetNodes(string html, IEnumerable<string> xpaths)
        {
            HtmlDocument htmlDoc = GetHtmlDocument(html);

            return GetNodes(htmlDoc, xpaths);
        }

        /// <summary>
        /// Parses the html for the XPath expressions.
        /// </summary>
        /// <param name="html">The html to parse.</param>
        /// <param name="xpaths">The XPath expressions.</param>
        /// <returns>
        /// The <c>HtmlNodes</c> found.
        /// </returns>
        public static IEnumerable<HtmlNode> GetNodes(HtmlDocument html, IEnumerable<string> xpaths)
        {
            var nodes = new List<HtmlNode>();
            
            foreach (var xpath in xpaths)
            {
                nodes.AddRange(GetNodes(html, xpath));
            }
            
            return nodes;
        }

        /// <summary>
        /// Parses the html for the XPath.
        /// </summary>
        /// <param name="html">The html to parse.</param>
        /// <param name="xpath">The XPath.</param>
        /// <returns>
        /// The <c>HtmlNodes</c> found.
        /// </returns>
        public static HtmlNodeCollection GetNodes(string html, string xpath)
        {
            HtmlDocument htmlDoc = GetHtmlDocument(html);

            return GetNodes(htmlDoc, xpath);
        }

        /// <summary>
        /// Parses the html for the XPath.
        /// </summary>
        /// <param name="html">The html to parse.</param>
        /// <param name="xpath">The XPath.</param>
        /// <returns>
        /// The <c>HtmlNodes</c> found.
        /// </returns>
        public static HtmlNodeCollection GetNodes(HtmlDocument html, string xpath)
        {
            xpath = xpath.Replace("&", "&amp;");

            // Find elements at XPath
            var nodes = html.DocumentNode.SelectNodes(xpath);

            // If the xpath returned zero nodes and the xpath contains "tbody"
            if (nodes == null && xpath.Contains("tbody"))
            {
                xpath = Regex.Replace(xpath, "//?(following|following-sibling|child|descendant|descendant-or-self)::tbody//?", "//");
                xpath = Regex.Replace(xpath, "(::)(tbody)(//?)", "$1*$3");
                xpath = Regex.Replace(xpath, "//?tbody//?", "//");
                nodes = html.DocumentNode.SelectNodes(xpath);
            }

            return nodes;
        }

        /// <summary>
        /// Parses the html for the XPath.
        /// </summary>
        /// <param name="html">The html to parse.</param>
        /// <param name="xpath">The XPath.</param>
        /// <returns>
        /// The <c>HtmlNodes</c> found.
        /// </returns>
        public static HtmlNode GetNode(string html, string xpath)
        {
            HtmlDocument htmlDoc = GetHtmlDocument(html);

            return GetNode(htmlDoc, xpath);
        }

        /// <summary>
        /// Parses the html for the XPath.
        /// </summary>
        /// <param name="html">The html to parse.</param>
        /// <param name="xpath">The XPath.</param>
        /// <returns>
        /// The <c>HtmlNodes</c> found.
        /// </returns>
        public static HtmlNode GetNode(HtmlDocument html, string xpath)
        {
            xpath = xpath.Replace("&", "&amp;");

            // Find elements at XPath
            var node = html.DocumentNode.SelectSingleNode(xpath);

            // If the xpath returned zero nodes and the xpath contains "tbody"
            if (node == null && xpath.Contains("tbody"))
            {
                xpath = Regex.Replace(xpath, "//?(following|following-sibling|child|descendant|descendant-or-self)::tbody//?", "//");
                xpath = Regex.Replace(xpath, "(::)(tbody)(//?)", "$1*$3");
                xpath = Regex.Replace(xpath, "//?tbody//?", "//");
                node = html.DocumentNode.SelectSingleNode(xpath);
            }

            return node;
        }

        /// <summary>
        /// Removes the specified nodes from the html.
        /// </summary>
        /// <param name="html">The html.</param>
        /// <param name="xpath">The XPath.</param>
        /// <returns>The html with the nodes removed.</returns>
        public static HtmlDocument RemoveNode(HtmlDocument html, string xpath)
        {
            return RemoveNodes(html, new[] { xpath });
        }

        /// <summary>
        /// Removes the specified nodes from the html.
        /// </summary>
        /// <param name="html">The html.</param>
        /// <param name="xpaths">The XPath expressions.</param>
        /// <returns>The html with the nodes removed.</returns>
        public static HtmlDocument RemoveNodes(HtmlDocument html, IEnumerable<string> xpaths)
        {
            foreach (var xpath in xpaths)
            {
                html.DocumentNode.SelectSingleNode(xpath).Remove();
            }

            return html;
        }

        /// <summary>
        /// Creates an <c>HtmlDocument</c> with the html provided.
        /// </summary>
        /// <param name="html">The source.</param>
        /// <returns>
        /// An <c>HtmlDocument</c>.
        /// </returns>
        public static HtmlDocument GetHtmlDocument(string html)
        {
            if (html == null) return new HtmlDocument();
            var htmlDoc = new HtmlDocument { OptionFixNestedTags = true };
            HtmlNode.ElementsFlags.Remove("form");
            HtmlNode.ElementsFlags.Remove("option");
            htmlDoc.LoadHtml(html);
            return htmlDoc;
        }

        /// <summary>
        /// Constructs an XPath based on the <c>DefinedAttributes</c>.
        /// </summary>
        /// <param name="definedAttributes">
        /// The defined attributes.
        /// </param>
        /// <returns>
        /// The XPath.
        /// </returns>
        public static string GetXpathFromDefinedAttributes(DefinedAttributes definedAttributes)
        {
            return string.Format("//*{0}", GetAttributeStringFromDefinedAttributes(definedAttributes));
        }

        /// <summary>
        /// Constructs an XPath based on the <c>DomNode</c>.
        /// </summary>
        /// <param name="domNode">
        /// The defined attributes.
        /// </param>
        /// <returns>
        /// The XPath.
        /// </returns>
        public static string GetXpathFromDomNode(DomNode domNode)
        {
            return GetXpathFromDomNodes(domNode.XpathNodes.ToArray());
        }

        /// <summary>
        /// Constructs an XPath based on the <c>DomNodes</c>.
        /// </summary>
        /// <param name="domNodes">
        /// The defined attributes.
        /// </param>
        /// <returns>
        /// The XPath.
        /// </returns>
        public static string GetXpathFromDomNodes(DomNodes domNodes)
        {
            return GetXpathFromDomNodes(domNodes.XpathNodes.ToArray());
        }

        /// <summary>
        /// Determines the XPath of the passed <c>Locator</c>.
        /// </summary>
        /// <param name="locator">The locator.</param>
        /// <returns>
        /// The XPath.
        /// </returns>
        /// <exception cref="Exception">
        /// Not implemented.
        /// </exception>
        public static string GetXpathFromLocator(Locator locator)
        {
            return GetXpathFromLocator(locator, false);
        }
        
        /// <summary>
        /// Determines if the XPath is valid.
        /// </summary>
        /// <param name="xpath">
        /// The XPath.
        /// </param>
        /// <returns>
        /// The validated XPath.
        /// </returns>
        /// <exception cref="XPathException">
        /// XPath is not valid.
        /// </exception>
        public static string ValidateXpath(string xpath)
        {
            var doc = new XmlDocument();
            var nav = doc.CreateNavigator();
            try
            {
                nav.Compile(xpath);
            }
            catch
            {
                throw new XPathException(string.Format("XPath is not valid: {0}", xpath));
            }

            return xpath;
        }

        /// <summary>
        /// Validates the XPath, throws an error if the xpath is invalid.
        /// </summary>
        /// <param name="xpath">The XPath to validate.</param>
        /// <returns>A boolean value indicating that the XPath is valid: true, or invalid: false.</returns>
        public static bool XpathIsValid(string xpath)
        {
            bool valid = true;
            try
            {
                ValidateXpath(xpath);
            }
            catch
            {
                valid = false;
            }

            return valid;
        }

        /// <summary>
        /// Produce an XPath literal equal to the value if possible; if not, produce
        /// an XPath expression that will match the value.
        /// This function will produce very long XPath expressions if a value
        /// contains a long run of double quotes.
        /// </summary>
        /// <param name="value">The value to match.</param>
        /// <returns>If the value contains only single or double quotes, an XPath
        /// literal equal to the value.  If it contains both, an XPath expression,
        /// using concat(), that evaluates to the value.</returns>
        public static string XPathLiteral(string value)
        {
            if (!value.Contains("'"))
            {
                return "'" + value + "'";
            }

            if (!value.Contains("\""))
            {
                return "\"" + value + "\"";
            }

            var sb = new StringBuilder();
            sb.Append("concat(");
            var substrings = value.Split('\"');
            for (var i = 0; i < substrings.Length; i++)
            {
                var needComma = i > 0;
                if (substrings[i] != string.Empty)
                {
                    if (i > 0)
                    {
                        sb.Append(", ");
                    }

                    sb.Append("\"");
                    sb.Append(substrings[i]);
                    sb.Append("\"");
                    needComma = true;
                }

                if (i < substrings.Length - 1)
                {
                    if (needComma)
                    {
                        sb.Append(", ");
                    }

                    sb.Append("'\"'");
                }
            }

            sb.Append(")");
            return sb.ToString();
        }

        /// <summary>
        /// Relocates the remote resource to a local location and updates the html to point to the new location.
        /// </summary>
        /// <param name="client">The client.</param>
        /// <param name="html">The html.</param>
        /// <param name="xpath">The XPath.</param>
        /// <param name="attribute">The attribute.</param>
        /// <param name="host">The host.</param>
        /// <param name="resourceFolder">The name of the resource folder.</param>
        /// <param name="resourceName">The name of the resource.</param>
        /// <param name="destPath">The path to the folder that contains the resource directory.</param>
        /// <returns>The modified html.</returns>
        public static string RelocateResourceToLocal(WebRequestClient client, string html, string xpath, string attribute, string host, string resourceFolder, string resourceName, string destPath)
        {
            var htmlDoc = GetHtmlDocument(html);
            htmlDoc = RelocateResourceToLocal(client, htmlDoc, xpath, attribute, host, resourceFolder, resourceName, destPath);

            return htmlDoc.DocumentNode.OuterHtml;
        }

        /// <summary>
        /// Relocates the remote resource to a local location and updates the html to point to the new location.
        /// </summary>
        /// <param name="client">The client.</param>
        /// <param name="html">The html.</param>
        /// <param name="xpath">The XPath.</param>
        /// <param name="attribute">The attribute.</param>
        /// <param name="host">The host.</param>
        /// <param name="resourceFolder">The name of the resource folder.</param>
        /// <param name="resourceName">The name of the resource.</param>
        /// <param name="destPath">The path to the folder that contains the resource directory.</param>
        /// <returns>The modified html.</returns>
        public static HtmlDocument RelocateResourceToLocal(WebRequestClient client, HtmlDocument html, string xpath, string attribute, string host, string resourceFolder, string resourceName, string destPath)
        {
            // Get node
            var node = GetNode(html, xpath);

            // Relocate node resource
            RelocateNodeResourceToLocal(client, node, attribute, host, resourceFolder, resourceName, destPath);
            
            return html;
        }

        /// <summary>
        /// Relocates the remote resources to a local location and updates the html to point to the new location.
        /// </summary>
        /// <param name="client">The client.</param>
        /// <param name="html">The html.</param>
        /// <param name="xpath">The XPath.</param>
        /// <param name="attribute">The attribute.</param>
        /// <param name="host">The host.</param>
        /// <param name="resourceFolder">The name of the resource folder.</param>
        /// <param name="destPath">The path to the folder that contains the resource directory.</param>
        /// <returns>The modified html.</returns>
        public static string RelocateResourcesToLocal(WebRequestClient client, string html, string xpath, string attribute, string host, string resourceFolder, string destPath)
        {
            var htmlDoc = GetHtmlDocument(html);
            htmlDoc = RelocateResourcesToLocal(client, htmlDoc, xpath, attribute, host, resourceFolder, destPath);

            return htmlDoc.DocumentNode.OuterHtml;
        }

        /// <summary>
        /// Relocates the remote resources to a local location and updates the html to point to the new location.
        /// </summary>
        /// <param name="client">The client.</param>
        /// <param name="html">The html.</param>
        /// <param name="xpath">The XPath.</param>
        /// <param name="attribute">The attribute.</param>
        /// <param name="host">The host.</param>
        /// <param name="resourceFolder">The name of the resource folder.</param>
        /// <param name="destPath">The path to the folder that contains the resource directory.</param>
        /// <returns>The modified html.</returns>
        public static HtmlDocument RelocateResourcesToLocal(WebRequestClient client, HtmlDocument html, string xpath, string attribute, string host, string resourceFolder, string destPath)
        {
            var i = 0;
            foreach (var node in html.DocumentNode.SelectNodes(xpath))
            {
                // Get resource uri
                var resourceSrc = GetAttributeValue(node, attribute);

                // Find a friendly resource name
                var resourceName = Regex.Match(resourceSrc, @"[^/]*\.[a-zA-Z0-9]*").Value;

                // If resource name isn't friendly then assign one
                resourceName = string.IsNullOrWhiteSpace(resourceName) ? string.Format("{0}", i++) : resourceName;

                // Relocate node resource
                RelocateNodeResourceToLocal(client, node, attribute, host, resourceFolder, resourceName, destPath);
            }

            return html;
        }

        #region Helper Methods

        /// <summary>
        /// Relocates the resource referenced by a node to a local location.
        /// </summary>
        /// <param name="client">The client.</param>
        /// <param name="node">The node.</param>
        /// <param name="attribute">The attribute.</param>
        /// <param name="host">The host.</param>
        /// <param name="resourceFolder">The name of the resource folder.</param>
        /// <param name="resourceName">The name of the resource.</param>
        /// <param name="destPath">The path to the folder that contains the resource directory.</param>
        private static void RelocateNodeResourceToLocal(WebRequestClient client, HtmlNode node, string attribute, string host, string resourceFolder, string resourceName, string destPath)
        {
            // Get the resource uri
            var resourceLocation = GetAttributeValue(node, attribute);

            // Delete the file if it already exists
            var file = Path.Combine(destPath, resourceFolder, resourceName);
            File.Delete(file);

            // Retrieve the file
            client.DoGetFile(string.Format("{0}{1}", host, resourceLocation), file);

            // Update resource uri in html
            SetAttributeValue(node, attribute, Path.Combine(resourceFolder, resourceName));
        }

        /// <summary>
        /// Replaces character codes with their respective values.
        /// </summary>
        /// <param name="value">The input string.</param>
        /// <returns>The string with the character codes replaced.</returns>
        private static string ReplaceKnownCharacterCodes(string value)
        {
            return value.Replace("&#39;", "'").Replace("&amp;", "&");
        }

        /// <summary>
        /// Constructs an XPath based on the <c>DefinedAttributes</c>.
        /// </summary>
        /// <param name="definedAttributes">
        /// The defined attributes.
        /// </param>
        /// <returns>
        /// The XPath.
        /// </returns>
        internal static string GetAttributeStringFromDefinedAttributes(DefinedAttributes definedAttributes)
        {
            string attrStr = string.Empty;
            foreach (var definedAttribute in definedAttributes.Attributes)
            {
                var attribute = definedAttribute.AttributeIs == AttributeIs.Custom
                                    ? definedAttribute.CustomAttribute
                                    : definedAttribute.AttributeIs.ToString().ToLower();
                string attributeMatcher;
                switch (definedAttribute.AttributeIs)
                {
                    case AttributeIs.Text:
                        attributeMatcher = "text()";
                        break;
                    case AttributeIs.Any:
                        attributeMatcher = "@*";
                        break;
                    default:
                        attributeMatcher = string.Format("@{0}", attribute);
                        break;
                }

                if (definedAttribute.Or)
                {
                    if (!definedAttribute.Exists)
                    {
                        var attr = !definedAttribute.Contains 
                                ? string.Format(" or {0}]", GetAttributeComparison(definedAttribute, attributeMatcher)) 
                                : string.Format(" or contains({0},{1})]", attributeMatcher, XPathLiteral(definedAttribute.Value));
                        attrStr = string.Format("{0}{1}", attrStr.TrimEnd(']'), attr);
                    }
                    else
                    {
                        attrStr = string.Format("{0} or {1}]", attrStr.TrimEnd(']'), attributeMatcher);
                    }
                }
                else
                {
                    if (!definedAttribute.Exists)
                    {
                        attrStr = !definedAttribute.Contains
                            ? string.Format("{0}[{1}]", attrStr, GetAttributeComparison(definedAttribute, attributeMatcher))
                            : string.Format("{0}[contains({1},{2})]", attrStr, attributeMatcher, XPathLiteral(definedAttribute.Value));
                    }
                    else
                    {
                        attrStr = string.Format("{0}[{1}]", attrStr, attributeMatcher);
                    }
                }
            }

            return attrStr;
        }

        private static string GetAttributeComparison(DefineAttribute definedAttribute, string attribute)
        {
            string attributeComparison;
            switch (definedAttribute.AttributeIs)
            {
                case AttributeIs.Class:
                    attributeComparison = string.Format("contains(concat(' ', normalize-space(@class), ' '), {0})", XPathLiteral(string.Format(" {0} ", definedAttribute.Value)));
                    break;
                default:
                    attributeComparison = string.Format("{0}={1}", attribute, XPathLiteral(definedAttribute.Value));
                    break;
            }

            return attributeComparison;
        }

        /// <summary>
        /// Determines the XPath of the passed <c>Locator</c>.
        /// </summary>
        /// <param name="locator">The locator.</param>
        /// <param name="ignoreIndex">Whether to include the <c>Index</c> of the <c>Locator</c> in the XPath.</param>
        /// <returns>
        /// The XPath.
        /// </returns>
        /// <exception cref="Exception">
        /// Not implemented.
        /// </exception>
        internal static string GetXpathFromLocator(Locator locator, bool ignoreIndex)
        {
            string compiledXpath;

            switch (locator.FindBy)
            {
                case FindBy.Id:
                    compiledXpath = locator.IsPartial
                                        ? string.Format("//*[contains(@id, '{0}')]", locator.Value)
                                        : string.Format("//*[@id='{0}']", locator.Value);
                    break;
                case FindBy.Name:
                    compiledXpath = locator.IsPartial
                                        ? string.Format("//*[contains(@name, '{0}')]", locator.Value)
                                        : string.Format("//*[@name='{0}']", locator.Value);
                    break;
                case FindBy.Value:
                    compiledXpath = locator.IsPartial
                                        ? string.Format("//input[contains(@Value, '{0}')]", locator.Value)
                                        : string.Format("//input[@Value='{0}']", locator.Value);
                    break;
                case FindBy.Text:
                    compiledXpath = locator.IsPartial
                                        ? string.Format("//*[contains(normalize-space(text()), '{0}')]", locator.Value)
                                        : string.Format("//*[normalize-space(text())='{0}']", locator.Value);
                    break;
                case FindBy.Class:
                    compiledXpath = locator.IsPartial
                                        ? string.Format("//*[contains(@class, '{0}')]", locator.Value)
                                        : string.Format("//*[contains(concat(' ', normalize-space(@class), ' '), {0})]", XPathLiteral(string.Format(" {0} ", locator.Value)));
                    break;
                case FindBy.Href:
                    compiledXpath = locator.IsPartial
                                        ? string.Format("//a[contains(@href,'{0}')]", locator.Value)
                                        : string.Format("//a[@href='{0}']", locator.Value);
                    break;
                case FindBy.Css:
                    compiledXpath = _cx.Transform(locator.Value);
                    break;
                case FindBy.Alt:
                    compiledXpath = locator.IsPartial
                                        ? string.Format("//*[contains(@alt, '{0}')]", locator.Value)
                                        : string.Format("//*[@alt='{0}']", locator.Value);
                    break;
                case FindBy.XPath:
                    compiledXpath = locator.Value;
                    break;
                default:
                    throw new Exception("Not implemented for Locators of type: " + locator.FindBy);
            }

            // Respect the index property
            if (locator.Use == Use.One && locator.Index > 0 && !ignoreIndex)
            {
                compiledXpath = string.Format("({0})[{1}]", compiledXpath, locator.Index);
            }

            return ValidateXpath(compiledXpath);
        }

        /// <summary>
        /// Generates an XPath from the passed XPath nodes.
        /// </summary>
        /// <param name="xpathNodes">
        /// The xpath nodes.
        /// </param>
        /// <returns>
        /// An XPath expression.
        /// </returns>
        private static string GetXpathFromDomNodes(IEnumerable<XpathNode> xpathNodes)
        {
            string nodeStr = string.Empty;
            foreach (var node in xpathNodes)
            {
                string temp = null;
                switch (node.NodeIs)
                {
                    case NodeIs.First:
                        temp = "[1]";
                        break;
                    case NodeIs.Position:
                        var attrs = node.DefinedAttributes;
                        if (attrs != null)
                        {
                            temp = string.Format("[{0}]", attrs.Attributes.First().Value);
                        }

                        break;
                    case NodeIs.Last:
                        temp = "[last()]";
                        break;
                    case NodeIs.Custom:
                        temp = node.CustomNode.ToLower().Trim();
                        break;
                    case NodeIs.Any:
                        temp = "*";
                        break;
                    default:
                        temp = _nodes[node.NodeIs];
                        break;
                }

                var isPositioner = node.NodeIs.In(NodeIs.First, NodeIs.Last, NodeIs.Position);

                if (node.DefinedAttributes != null && node.DefinedAttributes.Attributes != null
                    && node.DefinedAttributes.Attributes.Count > 0 && !isPositioner)
                {
                    temp = string.Format(
                        "{0}{1}", temp, GetAttributeStringFromDefinedAttributes(node.DefinedAttributes));
                }

                nodeStr = string.Format(isPositioner ? "({0}){1}" : "{0}//{1}", nodeStr, temp);
            }

            return nodeStr.Trim();
        }

        #endregion
    }
}